package com.cmeduSystem.common.online.script;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.cmeduSystem.common.core.constant.AggregationType;
import com.cmeduSystem.common.core.constant.DictType;
import com.cmeduSystem.common.core.exception.MyRuntimeException;
import com.cmeduSystem.common.core.object.MyPageData;
import com.cmeduSystem.common.core.object.TokenData;
import com.cmeduSystem.common.core.util.MyCommonUtil;
import com.cmeduSystem.common.online.model.OnlineApi;
import com.cmeduSystem.common.online.model.OnlineDblink;
import com.cmeduSystem.common.online.model.constant.RelationType;
import com.cmeduSystem.common.online.object.ConstDictInfo;
import com.cmeduSystem.common.online.service.OnlineApiService;
import com.cmeduSystem.common.online.service.OnlineDblinkService;
import com.cmeduSystem.common.online.util.OnlineApiOperationHelper;
import com.cmeduSystem.common.online.util.OnlineDataSourceUtil;
import com.cmeduSystem.common.redis.util.CommonRedisUtil;
import com.cmeduSystem.common.sequence.wrapper.IdGeneratorWrapper;
import lombok.extern.slf4j.Slf4j;
import org.mozilla.javascript.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 在线表单API脚本帮助类。
 *
 * @author xulei
 * @date 2025-09-04
 */
@Slf4j
@Component
public class OnlineApiScriptHelper {

    @Autowired
    private OnlineDblinkService onlineDblinkService;
    @Autowired
    private OnlineApiService onlineApiService;
    @Autowired
    private OnlineDataSourceUtil dataSourceUtil;
    @Autowired
    private OnlineApiOperationHelper onlineApiOperationHelper;
    @Autowired
    private OnlineApiScriptHelper onlineApiScriptHelper;
    @Autowired
    private IdGeneratorWrapper idGenerator;
    @Autowired
    private CommonRedisUtil commonRedisUtil;

    private static final String SCRIPT_USEDBLINK_KEY = "Dblink";
    private static final String SCRIPT_CONNECTION_KEY = "Connection";

    /**
     * 执行脚本。
     *
     * @param id          脚本Id。
     * @param scriptParam 脚本参数。
     * @return 执行结果。
     */
    public Object runScript(Long id, JSONObject scriptParam) {
        return this.doRunScript(onlineApiService.getById(id), scriptParam);
    }

    /**
     * 通过脚本编码执行脚本。
     *
     * @param code        脚本编码。
     * @param scriptParam 脚本参数。
     * @return 执行结果。
     */
    public Object runScript(String code, JSONObject scriptParam) {
        OnlineApi filter = new OnlineApi();
        filter.setApiCode(code);
        return this.doRunScript(onlineApiService.getOne(filter), scriptParam);
    }

    /**
     * 执行JS脚本。
     *
     * @param script 脚本内容。
     * @return 执行结果。
     */
    public Object runScript(String script) {
        try (Context context = Context.enter()) {
            // 创建一个全局作用域
            Scriptable scope = context.initStandardObjects();
            ScriptableObject.putProperty(scope, "DictType",
                    Context.javaToJS(ReflectUtil.newInstance(DictType.class), scope));
            ScriptableObject.putProperty(scope, "RelationType",
                    Context.javaToJS(ReflectUtil.newInstance(RelationType.class), scope));
            ScriptableObject.putProperty(scope, "AggregationType",
                    Context.javaToJS(ReflectUtil.newInstance(AggregationType.class), scope));
            ScriptableObject.putProperty(scope, "api",
                    Context.javaToJS(onlineApiScriptHelper, scope));
            ScriptableObject.putProperty(scope, "idUtil",
                    Context.javaToJS(idGenerator, scope));
            ScriptableObject.putProperty(scope, "commonRedisUtil",
                    Context.javaToJS(commonRedisUtil, scope));
            ScriptableObject.putProperty(scope, "Date", Context.javaToJS(Date.class, scope));
            TokenData tokenData = TokenData.takeFromRequest();
            if (tokenData != null) {
                ScriptableObject.putProperty(scope, "tokenData", Context.javaToJS(tokenData, scope));
            }
            // 使用defineClass方法将MyPageParam类暴露给JavaScript
            JsPageParam.convertToJsConstructor(scope);
            JsPageData.convertToJsConstructor(scope);
            Context.getCurrentContext().putThreadLocal("scope", scope);
            JsQueryBuilder.convertToJsConstructor(scope);
            ScriptableObject.defineClass(scope, JsPageParam.class);
            ScriptableObject.defineClass(scope, JsPageData.class);
            try {
                Object jsResult = context.evaluateString(scope, script, "<cmd>", 1, null);
                Object result = Context.toObject(jsResult, scope);
                this.closeDblink(true);
                return result;
            } catch (Exception e) {
                this.closeDblink(context, false);
                throw new MyRuntimeException(e);
            }
        } catch (Exception e) {
            this.closeDblink(false);
            throw new MyRuntimeException(e);
        }
    }

    /**
     * 打开数据库链接。
     *
     * @param dblinkName 数据库链接名。
     * @param beginTrans 是否手动开启事务。
     */
    public void useDblink(String dblinkName, boolean beginTrans) {
        OnlineDblink filter = new OnlineDblink();
        filter.setDblinkName(dblinkName);
        List<OnlineDblink> dblinkList = onlineDblinkService.getListByFilter(filter);
        if (CollUtil.isEmpty(dblinkList)) {
            throw new MyRuntimeException("数据库链接名 [" + dblinkName + "] 不存在.");
        }
        if (dblinkList.size() > 1) {
            throw new MyRuntimeException("数据库链接名 [" + dblinkName + "] 大于1.");
        }
        OnlineDblink dblink = dblinkList.get(0);
        try {
            Connection connection = dataSourceUtil.getConnection(dblink.getDblinkId());
            if (beginTrans) {
                connection.setAutoCommit(false);
            }
            Context.getCurrentContext().putThreadLocal(SCRIPT_USEDBLINK_KEY, dblink);
            Context.getCurrentContext().putThreadLocal(SCRIPT_CONNECTION_KEY, connection);
        } catch (Exception e) {
            throw new MyRuntimeException(e);
        }
    }

    /**
     * 关闭数据库链接。
     *
     * @param cx     脚本的上下文环境。
     * @param commit 是否提交事务。
     */
    public void closeDblink(Context cx, boolean commit) {
        if (cx == null) {
            return;
        }
        Connection connection = null;
        try {
            connection = (Connection) Context.getCurrentContext().getThreadLocal(SCRIPT_CONNECTION_KEY);
            if (connection == null) {
                return;
            }
            if (!connection.getAutoCommit()) {
                if (commit) {
                    connection.commit();
                } else {
                    connection.rollback();
                }
            }
        } catch (SQLException e) {
            throw new MyRuntimeException(e);
        } finally {
            if (connection != null) {
                try {
                    connection.close();
                } catch (SQLException e) {
                    log.error(e.getMessage(), e);
                }
            }
            cx.removeThreadLocal(SCRIPT_CONNECTION_KEY);
            cx.removeThreadLocal(SCRIPT_USEDBLINK_KEY);
        }
    }

    /**
     * 关闭数据库链接。
     *
     * @param commit 是否提交事务。
     */
    public void closeDblink(boolean commit) {
        Context cx = Context.getCurrentContext();
        this.closeDblink(cx, commit);
    }

    /**
     * 执行增删改SQL。调用该方法，必须先调用useDblink方法指定数据库链接。
     *
     * @param sql 待执行的SQL语句。
     * @return 更新数量。
     */
    public Integer execute(String sql) {
        Connection conn = this.getConnectionWithSafe();
        return dataSourceUtil.execute(conn, sql);
    }

    /**
     * 执行增删改SQL。调用该方法，必须先调用useDblink方法指定数据库链接。
     *
     * @param sql    待执行的SQL语句。
     * @param params 参数列表。
     * @return 更新数量。
     */
    public Integer execute(String sql, NativeArray params) {
        if (CollUtil.isEmpty(params)) {
            return this.execute(sql);
        }
        List<Object> paramList = new LinkedList<>();
        for (int i = 0; i < params.getLength(); i++) {
            paramList.add(params.get(i));
        }
        Connection conn = this.getConnectionWithSafe();
        return dataSourceUtil.execute(conn, sql, paramList);
    }

    /**
     * 通过ApiQueryBuilder对象执行查询。调用该方法，必须先调用useDblink方法指定数据库链接。
     *
     * @param apiQueryBuilder 来自JS脚本的查询构建器参数。
     * @return 查询结果集的分页对象。
     */
    public JsPageData executeQuery(JsQueryBuilder apiQueryBuilder) {
        ApiQueryBuilder b = apiQueryBuilder.unwrap();
        try {
            MyPageData<Map<String, Object>> pageData;
            Connection conn = this.getConnectionWithSafe();
            if (b.getPageParam() != null) {
                pageData = dataSourceUtil.queryWithPage(
                        this.getDblinkIdWithSafe(), conn, b.getSql(), b.getParamList(), b.getPageParam());
            } else {
                List<Map<String, Object>> dataList = dataSourceUtil.query(conn, b.getSql(), b.getParamList());
                pageData = new MyPageData<>(dataList, (long) dataList.size());
            }
            if (b.getJsRelations() != null) {
                this.buildRelationInternal(pageData.getDataList(), b.getJsRelations());
            }
            if (b.getJsDictRelations() != null) {
                this.buildDictRelationInternal(pageData.getDataList(), b.getJsDictRelations());
            }
            return this.makeJsPageDataForJsScript(apiQueryBuilder, pageData);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new MyRuntimeException(e);
        }
    }

    /**
     * 在当前已经打开的数据库链接中执行SQL查询。调用该方法，必须先调用useDblink方法指定数据库链接。
     *
     * @param sql SQL查询语句。
     * @return 查询的结果集。
     */
    public NativeArray executeQuery(String sql) {
        try {
            List<Map<String, Object>> dataList = dataSourceUtil.query(this.getConnectionWithSafe(), sql);
            return this.convertToJsNativeArray(dataList);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new MyRuntimeException(e);
        }
    }

    /**
     * 在当前已经打开的数据库链接中执行SQL查询。调用该方法，必须先调用useDblink方法指定数据库链接。
     *
     * @param sql    SQL查询语句。
     * @param params 参数列表。
     * @return 查询的结果集。
     */
    public NativeArray executeQuery(String sql, NativeArray params) {
        try {
            List<Object> paramList = this.convertJsNativeArrayToJavaList(params);
            List<Map<String, Object>> dataList = dataSourceUtil.query(this.getConnectionWithSafe(), sql, paramList);
            return this.convertToJsNativeArray(dataList);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new MyRuntimeException(e);
        }
    }

    /**
     * 在当前已经打开的数据库链接中执行SQL查询。调用该方法，必须先调用useDblink方法指定数据库链接。
     *
     * @param sql       SQL查询语句。
     * @param pageParam 分页参数。
     * @return 查询结果集的分页对象。
     */
    public JsPageData executeQuery(String sql, JsPageParam pageParam) {
        return this.executeQuery(sql, null, pageParam);
    }

    /**
     * 在当前已经打开的数据库链接中执行SQL查询。调用该方法，必须先调用useDblink方法指定数据库链接。
     *
     * @param sql       SQL查询语句。
     * @param params    参数列表。
     * @param pageParam 分页参数。
     * @return 查询结果集的分页对象。
     */
    public JsPageData executeQuery(String sql, NativeArray params, JsPageParam pageParam) {
        List<Object> paramList = this.convertJsNativeArrayToJavaList(params);
        try {
            MyPageData<Map<String, Object>> pageData = dataSourceUtil.queryWithPage(
                    this.getDblinkIdWithSafe(), this.getConnectionWithSafe(), sql, paramList, pageParam.unwrap());
            return this.makeJsPageDataForJsScript(pageParam, pageData);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new MyRuntimeException(e);
        }
    }

    /**
     * 绑定数据关联。关联参数为JS对象类型。
     *
     * @param dataList    主表数据列表。
     * @param jsRelations JS类型的关联数据列表。
     */
    public void buildRelation(NativeArray dataList, NativeArray jsRelations) {
        List<Map<String, Object>> tmpDataList = this.convertJsNativeArrayToJavaListMap(dataList);
        this.buildRelationInternal(tmpDataList, jsRelations);
        this.reloadJavaListMapToJsNatvieArray(tmpDataList, dataList);
    }

    /**
     * 绑定数据关联。关联参数为JS对象类型。
     *
     * @param dataList    主表数据列表。
     * @param jsRelations JS类型的关联数据列表。
     */
    public void buildDictRelation(NativeArray dataList, NativeArray jsRelations) {
        List<Map<String, Object>> tmpDataList = this.convertJsNativeArrayToJavaListMap(dataList);
        this.buildDictRelationInternal(tmpDataList, jsRelations);
        this.reloadJavaListMapToJsNatvieArray(tmpDataList, dataList);
    }

    /**
     * 转换JS本地对象为Java的Map对象。
     *
     * @param jsObject JS对象。
     * @return 转换后的Java的Map对象。
     */
    public Map<String, Object> convertJsNativeObjectToJavaMap(NativeObject jsObject) {
        Map<String, Object> data = new HashMap<>();
        Object[] ids = jsObject.getIds();
        for (Object id : ids) {
            if (id instanceof String) {
                String key = (String) id;
                Object value = jsObject.get(key, jsObject);
                if (value instanceof NativeObject) {
                    // 如果值是嵌套的 JavaScript 对象，递归转换
                    data.put(key, convertJsNativeObjectToJavaMap((NativeObject) value));
                } else {
                    data.put(key, value);
                }
            }
        }
        return data;
    }

    /**
     * 转换JS本地数组为Java的Map对象列表。
     *
     * @param jsArray JS数组。
     * @return 转换后的Java的Map对象列表。
     */
    public List<Map<String, Object>> convertJsNativeArrayToJavaListMap(NativeArray jsArray) {
        List<Map<String, Object>> dataList = new LinkedList<>();
        for (int i = 0; i < jsArray.getLength(); i++) {
            NativeObject value = (NativeObject) jsArray.get(i);
            dataList.add(convertJsNativeObjectToJavaMap(value));
        }
        return dataList;
    }

    private Object doRunScript(OnlineApi api, JSONObject scriptParam) {
        if (api == null) {
            throw new MyRuntimeException("数据验证失败，脚本数据不存在！");
        }
        if (StrUtil.isBlank(api.getApiScript())) {
            return null;
        }
        String script = api.getApiScript();
        if (MapUtil.isNotEmpty(scriptParam)) {
            script = MyCommonUtil.replaceExistWithVariableData(script, scriptParam);
        }
        return this.runScript(script);
    }

    private void buildRelationInternal(List<Map<String, Object>> dataList, NativeArray jsRelations) {
        List<Map<String, Object>> javaRelationList = new LinkedList<>();
        for (int i = 0; i < jsRelations.getLength(); i++) {
            NativeObject jsRelation = (NativeObject) jsRelations.get(i);
            Map<String, Object> javaRelation = this.convertJsNativeObjectToJavaMap(jsRelation);
            javaRelationList.add(javaRelation);
        }
        List<OnlineApiOperationHelper.RelationData> relationDataList =
                BeanUtil.copyToList(javaRelationList, OnlineApiOperationHelper.RelationData.class);
        relationDataList.forEach(r -> r.setDblinkId(this.getDblinkIdWithSafe()));
        List<OnlineApiOperationHelper.RelationData> normalRelationDataList =
                relationDataList.stream().filter(r -> r.getAggregationType() == null).collect(Collectors.toList());
        if (CollUtil.isNotEmpty(normalRelationDataList)) {
            onlineApiOperationHelper.buildRelation(dataList, normalRelationDataList);
        }
        List<OnlineApiOperationHelper.RelationData> aggregationRelationDataList =
                relationDataList.stream().filter(r -> r.getAggregationType() != null).collect(Collectors.toList());
        if (CollUtil.isNotEmpty(aggregationRelationDataList)) {
            onlineApiOperationHelper.buildRelationAggregation(dataList, aggregationRelationDataList);
        }
    }

    private void buildDictRelationInternal(List<Map<String, Object>> dataList, NativeArray jsRelations) {
        List<Map<String, Object>> javaRelationList = new LinkedList<>();
        for (int i = 0; i < jsRelations.getLength(); i++) {
            NativeObject jsRelation = (NativeObject) jsRelations.get(i);
            Map<String, Object> javaRelation = this.convertJsNativeObjectToJavaMap(jsRelation);
            if (jsRelation.get("dictType").equals(DictType.CUSTOM)) {
                NativeArray dictCustomDataArray = (NativeArray) jsRelation.get("dictCustomData");
                ConstDictInfo constDictInfo = new ConstDictInfo();
                List<ConstDictInfo.ConstDictData> constDictDataList = new LinkedList<>();
                constDictInfo.setDictData(constDictDataList);
                for (int j = 0; j < dictCustomDataArray.getLength(); j++) {
                    NativeObject dictCustomData = (NativeObject) dictCustomDataArray.get(j);
                    Object id = dictCustomData.get("id");
                    String name = dictCustomData.get("name").toString();
                    ConstDictInfo.ConstDictData constDictData = new ConstDictInfo.ConstDictData();
                    if (id instanceof Number) {
                        constDictData.setId(((Number) id).longValue());
                    } else {
                        constDictData.setId(id);
                    }
                    constDictData.setName(name);
                    constDictDataList.add(constDictData);
                }
                javaRelation.put("dictCustomData", JSON.toJSONString(constDictInfo));
            }
            javaRelationList.add(javaRelation);
        }
        List<OnlineApiOperationHelper.RelationDictData> relationDictDataList =
                BeanUtil.copyToList(javaRelationList, OnlineApiOperationHelper.RelationDictData.class);
        relationDictDataList.stream()
                .filter(r -> r.getDictType().equals(DictType.TABLE))
                .forEach(r -> r.setDictDblinkId(this.getDblinkIdWithSafe()));
        onlineApiOperationHelper.buildDictRelation(dataList, relationDictDataList);
    }

    @SuppressWarnings("all")
    private void reloadJavaListMapToJsNatvieArray(List<Map<String, Object>> srcDataList, NativeArray targetDataList) {
        if (CollUtil.isNotEmpty(targetDataList)) {
            for (Integer index : targetDataList.getIndexIds()) {
                targetDataList.delete(index);
            }
        }
        for (int i = 0; i < srcDataList.size(); i++) {
            Map<String, Object> data = srcDataList.get(i);
            targetDataList.put(i, targetDataList, this.convertToJsNativeObject(data));
        }
    }

    private NativeArray convertToJsNativeArray(List<Map<String, Object>> dataList) {
        NativeArray result = new NativeArray(dataList.size());
        this.reloadJavaListMapToJsNatvieArray(dataList, result);
        return result;
    }

    private NativeObject convertToJsNativeObject(Map<String, Object> data) {
        NativeObject result = new NativeObject();
        for (Map.Entry<String, Object> entry : data.entrySet()) {
            result.put(entry.getKey(), result, entry.getValue());
        }
        return result;
    }

    private List<Object> convertJsNativeArrayToJavaList(NativeArray jsArray) {
        List<Object> resultList = new LinkedList<>();
        if (CollUtil.isNotEmpty(jsArray)) {
            for (int i = 0; i < jsArray.getLength(); i++) {
                resultList.add(jsArray.get(i));
            }
        }
        return resultList;
    }

    private JsPageData makeJsPageDataForJsScript(
            ScriptableObject scriptable, MyPageData<Map<String, Object>> pageData) {
        Scriptable scope = ScriptableObject.getTopLevelScope(scriptable);
        // 创建一个新的JsPageData实例并初始化
        Object[] args = new Object[]{
                this.convertToJsNativeArray(pageData.getDataList()), pageData.getTotalCount().intValue()};
        return (JsPageData) Context.getCurrentContext().newObject(scope, "MyPageData", args);
    }

    private Long getDblinkIdWithSafe() {
        Object dblink = Context.getCurrentContext().getThreadLocal(SCRIPT_USEDBLINK_KEY);
        if (dblink == null) {
            throw new MyRuntimeException("当前JavaScript脚本没有调用 [Dblink] 方法指定数据库链接.");
        }
        return ((OnlineDblink) dblink).getDblinkId();
    }

    private Connection getConnectionWithSafe() {
        Object connection = Context.getCurrentContext().getThreadLocal(SCRIPT_CONNECTION_KEY);
        if (connection == null) {
            throw new MyRuntimeException("当前JavaScript脚本没有调用 [Connection] 方法指定数据库链接.");
        }
        return (Connection) connection;
    }
}
